import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'dart:math' as math;

import 'package:flutter/services.dart';

typedef CommonIndexHintBuilder = Widget Function(BuildContext context, String tag);

abstract class CommonIndexBarDragListener {
  factory CommonIndexBarDragListener.create() => CommonIndexBarDragNotifier();
  ValueListenable<CommonIndexBarDragDetails> get dragDetails;
}

class CommonIndexBarDragNotifier implements CommonIndexBarDragListener {
  @override
  final ValueNotifier<CommonIndexBarDragDetails> dragDetails = ValueNotifier(CommonIndexBarDragDetails());
}

class CommonIndexBarDragDetails {
  static const int actionDown = 0;
  static const int actionUp = 1;
  static const int actionUpdate = 2;
  static const int actionEnd = 3;
  static const int actionCancel = 4;

  int? action;
  int? index;
  String? tag;

  double? localPositionY;
  double? globalPositionY;

  CommonIndexBarDragDetails({
    this.action,
    this.index,
    this.tag,
    this.localPositionY,
    this.globalPositionY,
  });
}

const List<String> kIndexBarData = [
  'A',
  'B',
  'C',
  'D',
  'E',
  'F',
  'G',
  'H',
  'I',
  'J',
  'K',
  'L',
  'M',
  'N',
  'O',
  'P',
  'Q',
  'R',
  'S',
  'T',
  'U',
  'V',
  'W',
  'X',
  'Y',
  'Z',
  '#'
];

const double kIndexBarWidth = 30;

const double kIndexBarItemHeight = 16;

class CommonIndexBarOptions {
  const CommonIndexBarOptions({
    this.needRebuild = false,
    this.ignoreDragCancel = false,
    this.hapticFeedback = false,
    this.color,
    this.downColor,
    this.decoration,
    this.downDecoration,
    this.textStyle = const TextStyle(fontSize: 12, color: Color(0xFF666666)),
    this.downTextStyle,
    this.selectTextStyle,
    this.downItemDecoration,
    this.selectItemDecoration,
    this.indexHintWidth = 72,
    this.indexHintHeight = 72,
    this.indexHintDecoration = const BoxDecoration(
      color: Colors.black87,
      shape: BoxShape.rectangle,
      borderRadius: BorderRadius.all(Radius.circular(6)),
    ),
    this.indexHintTextStyle = const TextStyle(fontSize: 24.0, color: Colors.white),
    this.indexHintChildAlignment = Alignment.center,
    this.indexHintAlignment = Alignment.center,
    this.indexHintPosition,
    this.indexHintOffset = Offset.zero,
    this.localImages = const [],
  });

  final bool needRebuild;

  final bool ignoreDragCancel;

  final bool hapticFeedback;

  final Color? color;

  final Color? downColor;

  final Decoration? decoration;

  final Decoration? downDecoration;

  final TextStyle textStyle;

  final TextStyle? downTextStyle;

  final TextStyle? selectTextStyle;

  final Decoration? downItemDecoration;

  final Decoration? selectItemDecoration;

  final double indexHintWidth;

  final double indexHintHeight;

  final Decoration indexHintDecoration;

  final Alignment indexHintAlignment;

  final Alignment indexHintChildAlignment;

  final TextStyle indexHintTextStyle;

  final Offset? indexHintPosition;

  final Offset indexHintOffset;

  final List<String> localImages;
}

class CommonIndexBarController {
  _CommonIndexBarState? _indexBarState;

  bool get isAttached => _indexBarState != null;

  void updateTagIndex(String tag) {
    _indexBarState?._updateTagIndex(tag);
  }

  void _attach(_CommonIndexBarState state) {
    _indexBarState = state;
  }

  void _detach() {
    _indexBarState = null;
  }
}

class CommonIndexBar extends StatefulWidget {
  const CommonIndexBar({
    Key? key,
    this.data = kIndexBarData,
    this.width = kIndexBarWidth,
    this.height,
    this.itemHeight = kIndexBarItemHeight,
    this.margin,
    this.indexHintBuilder,
    CommonIndexBarDragListener? indexBarDragListener,
    this.options = const CommonIndexBarOptions(),
    this.controller,
  })  : indexBarDragNotifier = indexBarDragListener as CommonIndexBarDragNotifier?,
        super(key: key);

  final List<String> data;

  final double width;

  final double? height;

  final double itemHeight;

  final EdgeInsetsGeometry? margin;

  final CommonIndexHintBuilder? indexHintBuilder;

  final CommonIndexBarDragNotifier? indexBarDragNotifier;

  final CommonIndexBarOptions options;

  final CommonIndexBarController? controller;

  @override
  State<CommonIndexBar> createState() => _CommonIndexBarState();
}

class _CommonIndexBarState extends State<CommonIndexBar> {
  static OverlayEntry? overlayEntry;

  double floatTop = 0;
  String indexTag = '';
  int selectIndex = 0;
  int action = CommonIndexBarDragDetails.actionEnd;

  @override
  void initState() {
    super.initState();
    widget.indexBarDragNotifier?.dragDetails.addListener(_valueChanged);
    widget.controller?._attach(this);
  }

  void _valueChanged() {
    if (widget.indexBarDragNotifier == null) return;
    CommonIndexBarDragDetails details = widget.indexBarDragNotifier!.dragDetails.value;
    selectIndex = details.index!;
    indexTag = details.tag!;
    action = details.action!;
    floatTop = details.globalPositionY! + widget.itemHeight / 2 - widget.options.indexHintHeight / 2;

    if (_isActionDown()) {
      _addOverlay(context);
    } else {
      _removeOverlay();
    }

    if (widget.options.needRebuild) {
      if (widget.options.ignoreDragCancel && action == CommonIndexBarDragDetails.actionCancel) {
      } else {
        setState(() {});
      }
    }
  }

  bool _isActionDown() {
    return action == CommonIndexBarDragDetails.actionDown || action == CommonIndexBarDragDetails.actionUpdate;
  }

  @override
  void dispose() {
    widget.controller?._detach();
    _removeOverlay();
    widget.indexBarDragNotifier?.dragDetails.removeListener(_valueChanged);
    super.dispose();
  }

  Widget _buildIndexHint(BuildContext context, String tag) {
    if (widget.indexHintBuilder != null) {
      return widget.indexHintBuilder!(context, tag);
    }
    Widget child;
    TextStyle textStyle = widget.options.indexHintTextStyle;
    List<String> localImages = widget.options.localImages;
    if (localImages.contains(tag)) {
      child = Image.asset(
        tag,
        width: textStyle.fontSize,
        height: textStyle.fontSize,
        color: textStyle.color,
      );
    } else {
      child = Text(tag, style: textStyle);
    }
    return Container(
      width: widget.options.indexHintWidth,
      height: widget.options.indexHintHeight,
      alignment: widget.options.indexHintChildAlignment,
      decoration: widget.options.indexHintDecoration,
      child: child,
    );
  }

  void _addOverlay(BuildContext context) {
    OverlayState overlayState = Overlay.of(context);
    if (overlayEntry == null) {
      overlayEntry = OverlayEntry(builder: (BuildContext ctx) {
        double left;
        double top;
        if (widget.options.indexHintPosition != null) {
          left = widget.options.indexHintPosition!.dx;
          top = widget.options.indexHintPosition!.dy;
        } else {
          if (widget.options.indexHintAlignment == Alignment.centerRight) {
            left = MediaQuery.of(context).size.width -
                kIndexBarWidth -
                widget.options.indexHintWidth +
                widget.options.indexHintOffset.dx;
            top = floatTop + widget.options.indexHintOffset.dy;
          } else if (widget.options.indexHintAlignment == Alignment.centerLeft) {
            left = kIndexBarWidth + widget.options.indexHintOffset.dx;
            top = floatTop + widget.options.indexHintOffset.dy;
          } else {
            left = MediaQuery.of(context).size.width / 2 -
                widget.options.indexHintWidth / 2 +
                widget.options.indexHintOffset.dx;
            top = MediaQuery.of(context).size.height / 2 -
                widget.options.indexHintHeight / 2 +
                widget.options.indexHintOffset.dy;
          }
        }
        return Positioned(
          left: left,
          top: top,
          child: Material(
            color: Colors.transparent,
            child: _buildIndexHint(ctx, indexTag),
          ),
        );
      });
      overlayState.insert(overlayEntry!);
    } else {
      overlayEntry?.markNeedsBuild();
    }
  }

  void _removeOverlay() {
    overlayEntry?.remove();
    overlayEntry = null;
  }

  Widget _buildItem(BuildContext context, int index) {
    String tag = widget.data[index];
    Decoration? decoration;
    TextStyle? textStyle;
    if (widget.options.downItemDecoration != null) {
      decoration = (_isActionDown() && selectIndex == index) ? widget.options.downItemDecoration : null;
      textStyle = (_isActionDown() && selectIndex == index) ? widget.options.downTextStyle : widget.options.textStyle;
    } else if (widget.options.selectItemDecoration != null) {
      decoration = (selectIndex == index) ? widget.options.selectItemDecoration : null;
      textStyle = (selectIndex == index) ? widget.options.selectTextStyle : widget.options.textStyle;
    } else {
      textStyle =
          _isActionDown() ? (widget.options.downTextStyle ?? widget.options.textStyle) : widget.options.textStyle;
    }

    Widget child;
    List<String> localImages = widget.options.localImages;
    if (localImages.contains(tag)) {
      child = Image.asset(
        tag,
        width: textStyle?.fontSize,
        height: textStyle?.fontSize,
        color: textStyle?.color,
      );
    } else {
      child = Text(tag, style: textStyle);
    }

    return Container(
      alignment: Alignment.center,
      decoration: decoration,
      child: child,
    );
  }

  void _updateTagIndex(String tag) {
    if (_isActionDown()) return;
    selectIndex = widget.data.indexOf(tag);
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      color: _isActionDown() ? widget.options.downColor : widget.options.color,
      decoration: _isActionDown() ? widget.options.downDecoration : widget.options.decoration,
      width: widget.width,
      height: widget.height,
      margin: widget.margin,
      alignment: Alignment.center,
      child: CommonBaseIndexBar(
        data: widget.data,
        width: widget.width,
        itemHeight: widget.itemHeight,
        hapticFeedback: widget.options.hapticFeedback,
        itemBuilder: (BuildContext context, int index) {
          return _buildItem(context, index);
        },
        indexBarDragNotifier: widget.indexBarDragNotifier,
      ),
    );
  }
}

class CommonBaseIndexBar extends StatefulWidget {
  const CommonBaseIndexBar({
    Key? key,
    this.data = kIndexBarData,
    this.width = kIndexBarWidth,
    this.itemHeight = kIndexBarItemHeight,
    this.hapticFeedback = false,
    this.textStyle = const TextStyle(fontSize: 12.0, color: Color(0xFF666666)),
    this.itemBuilder,
    this.indexBarDragNotifier,
  }) : super(key: key);

  /// index data.
  final List<String> data;

  /// IndexBar width(def:30).
  final double width;

  /// IndexBar item height(def:16).
  final double itemHeight;

  /// Haptic feedback.
  final bool hapticFeedback;

  /// IndexBar text style.
  final TextStyle textStyle;

  final IndexedWidgetBuilder? itemBuilder;

  final CommonIndexBarDragNotifier? indexBarDragNotifier;

  @override
  _CommonBaseIndexBarState createState() => _CommonBaseIndexBarState();
}

class _CommonBaseIndexBarState extends State<CommonBaseIndexBar> {
  int lastIndex = -1;
  int _widgetTop = 0;

  int _getIndex(double offset) {
    int index = offset ~/ widget.itemHeight;
    return math.min(index, widget.data.length - 1);
  }

  _triggerDragEvent(int action) {
    if (widget.hapticFeedback &&
        (action == CommonIndexBarDragDetails.actionDown || action == CommonIndexBarDragDetails.actionUpdate)) {
      HapticFeedback.vibrate();
    }
    widget.indexBarDragNotifier?.dragDetails.value = CommonIndexBarDragDetails(
      action: action,
      index: lastIndex,
      tag: widget.data[lastIndex],
      localPositionY: lastIndex * widget.itemHeight,
      globalPositionY: lastIndex * widget.itemHeight + _widgetTop,
    );
  }

  RenderBox? _getRenderBox(BuildContext context) {
    RenderObject? renderObject = context.findRenderObject();
    RenderBox? box;
    if (renderObject != null) {
      box = renderObject as RenderBox;
    }
    return box;
  }

  @override
  Widget build(BuildContext context) {
    List<Widget> children = List.generate(widget.data.length, (index) {
      Widget child = widget.itemBuilder == null
          ? Center(child: Text(widget.data[index], style: widget.textStyle))
          : widget.itemBuilder!(context, index);
      return SizedBox(
        width: widget.width,
        height: widget.itemHeight,
        child: child,
      );
    });

    return GestureDetector(
      onVerticalDragDown: (DragDownDetails details) {
        RenderBox? box = _getRenderBox(context);
        if (box == null) return;
        Offset topLeftPosition = box.localToGlobal(Offset.zero);
        _widgetTop = topLeftPosition.dy.toInt();
        int index = _getIndex(details.localPosition.dy);
        if (index >= 0) {
          lastIndex = index;
          _triggerDragEvent(CommonIndexBarDragDetails.actionDown);
        }
      },
      onVerticalDragUpdate: (DragUpdateDetails details) {
        int index = _getIndex(details.localPosition.dy);
        if (index >= 0 && lastIndex != index) {
          lastIndex = index;
          //HapticFeedback.lightImpact();
          //HapticFeedback.vibrate();
          _triggerDragEvent(CommonIndexBarDragDetails.actionUpdate);
        }
      },
      onVerticalDragEnd: (DragEndDetails details) {
        _triggerDragEvent(CommonIndexBarDragDetails.actionEnd);
      },
      onVerticalDragCancel: () {
        _triggerDragEvent(CommonIndexBarDragDetails.actionCancel);
      },
      onTapUp: (TapUpDetails details) {
        //_triggerDragEvent(IndexBarDragDetails.actionUp);
      },
      behavior: HitTestBehavior.translucent,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: children,
      ),
    );
  }
}
